<!DOCTYPE HTML>
<html>
<!--
https://bugzilla.mozilla.org/show_bug.cgi?id=1243846

Some tests ported from IntersectionObserver/polyfill/intersection-observer-test.html

Original license header:

Copyright 2016 Google Inc. All Rights Reserved.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
    http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<head>
  <meta charset="utf-8">
  <title>Test for Bug 1243846</title>
  <script type="application/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
  <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
</head>
<body onload="next()">
<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1243846">Mozilla Bug 1243846</a>
<p id="display"></p>
<pre id="test">
<script type="application/javascript">

  SpecialPowers.setIntPref("layout.visibility.min-notify-intersection-observers-interval-ms", 0);

  var tests = [];
  var curDescribeMsg = '';
  var curItMsg = '';

  function beforeEach_fn() { };
  function afterEach_fn() { };

  function before(fn) {
    fn();
  }

  function beforeEach(fn) {
    beforeEach_fn = fn;
  }

  function afterEach(fn) {
    afterEach_fn = fn;
  }

  function it(msg, fn) {
    tests.push({
      msg: `${msg} [${curDescribeMsg}]`,
      fn: fn
    });
  }

  var callbacks = [];
  function callDelayed(fn, delay) {
    callbacks.push({
      fn: fn,
      time: +new Date() + delay
    });
  }

  requestAnimationFrame(function tick() {
    var i = callbacks.length;
    while (i--) {
      var cb = callbacks[i];
      if (+new Date() >= cb.time) {
        SimpleTest.executeSoon(cb.fn);
        callbacks.splice(i, 1);
      }
    }
    requestAnimationFrame(tick);
  });

  function expect(val) {
    return {
      to: {
        throwException: function (regexp) {
          try {
            val();
            ok(false, `${curItMsg} - an exception should have beeen thrown`);
          } catch (e) {
            ok(regexp.test(e), `${curItMsg} - supplied regexp should match thrown exception`);
          }
        },
        get be() {
          var fn = function (expected) {
            is(val, expected, curItMsg);
          };
          fn.ok = function () {
            ok(val, curItMsg);
          };
          fn.greaterThan = function (other) {
            ok(val > other, `${curItMsg} - ${val} should be greater than ${other}`);
          };
          fn.lessThan = function (other) {
            ok(val < other, `${curItMsg} - ${val} should be less than ${other}`);
          };
          return fn;
        },
        eql: function (expected) {
          if (Array.isArray(expected)) {
            if (!Array.isArray(val)) {
              ok(false, curItMsg, `${curItMsg} - should be an array,`);
              return;
            }
            is(val.length, expected.length, curItMsg, `${curItMsg} - arrays should be the same length`);
            if (expected.length != val.length) {
              return;
            }
            for (var i = 0; i < expected.length; i++) {
              is(val[i], expected[i], `${curItMsg} - array elements at position ${i} should be equal`);
              if (expected[i] != val[i]) {
                return;
              }
            }
            ok(true);
          }
        },
      }
    }
  }

  function describe(msg, fn) {
    curDescribeMsg = msg;
    fn();
    curDescribeMsg = '';
  }

  function next() {
    var test = tests.shift();
    if (test) {
      console.log(test.msg);
      curItMsg = test.msg;
      var fn = test.fn;
      beforeEach_fn();
      if (fn.length) {
        fn(function () {
          afterEach_fn();
          next();
        });
      } else {
        fn();
        afterEach_fn();
        next();
      }
    } else {
      SimpleTest.finish();
    }
  }

  var sinon = {
    spy: function () {
      var callbacks = [];
      var fn = function () {
        fn.callCount++;
        fn.lastCall = { args: arguments };
        if (callbacks.length) {
          callbacks.shift()();
        }
      };
      fn.callCount = 0;
      fn.lastCall = { args: [] };
      fn.waitForNotification = (fn) => {
        callbacks.push(fn);
      };
      return fn;
    }
  };

  var ASYNC_TIMEOUT = 300;


  var io;
  var noop = function() {};


  // References to DOM elements, which are accessible to any test
  // and reset prior to each test so state isn't shared.
  var rootEl;
  var grandParentEl;
  var parentEl;
  var targetEl1;
  var targetEl2;
  var targetEl3;
  var targetEl4;
  var targetEl5;


  describe('IntersectionObserver', function() {

    before(function() {

    });


    beforeEach(function() {
      addStyles();
      addFixtures();
    });


    afterEach(function() {
      if (io && 'disconnect' in io) io.disconnect();
      io = null;

      window.onmessage = null;

      removeStyles();
      removeFixtures();
    });


    describe('constructor', function() {

      it('throws when callback is not a function', function() {
        expect(function() {
          io = new IntersectionObserver(null);
        }).to.throwException(/.*/i);
      });


      it('instantiates root correctly', function() {
        io = new IntersectionObserver(noop);
        expect(io.root).to.be(null);

        io = new IntersectionObserver(noop, {root: rootEl});
        expect(io.root).to.be(rootEl);
      });


      it('throws when root is not an Element', function() {
        expect(function() {
          io = new IntersectionObserver(noop, {root: 'foo'});
        }).to.throwException(/.*/i);
      });


      it('instantiates rootMargin correctly', function() {
        io = new IntersectionObserver(noop, {rootMargin: '10px'});
        expect(io.rootMargin).to.be('10px 10px 10px 10px');

        io = new IntersectionObserver(noop, {rootMargin: '10px -5%'});
        expect(io.rootMargin).to.be('10px -5% 10px -5%');

        io = new IntersectionObserver(noop, {rootMargin: '10px 20% 0px'});
        expect(io.rootMargin).to.be('10px 20% 0px 20%');

        io = new IntersectionObserver(noop, {rootMargin: '0px 0px -5% 5px'});
        expect(io.rootMargin).to.be('0px 0px -5% 5px');
      });


      it('throws when rootMargin is not in pixels or percent', function() {
        expect(function() {
          io = new IntersectionObserver(noop, {rootMargin: 'auto'});
        }).to.throwException(/pixels.*percent/i);
      });


      it('instantiates thresholds correctly', function() {
        io = new IntersectionObserver(noop);
        expect(io.thresholds).to.eql([0]);

        io = new IntersectionObserver(noop, {threshold: 0.5});
        expect(io.thresholds).to.eql([0.5]);

        io = new IntersectionObserver(noop, {threshold: [0.25, 0.5, 0.75]});
        expect(io.thresholds).to.eql([0.25, 0.5, 0.75]);

        io = new IntersectionObserver(noop, {threshold: [1, .5, 0]});
        expect(io.thresholds).to.eql([0, .5, 1]);
      });

      it('throws when a threshold value is not between 0 and 1', function() {
        expect(function() {
          io = new IntersectionObserver(noop, {threshold: [0, -1]});
        }).to.throwException(/threshold/i);
      });

      it('throws when a threshold value is not a number', function() {
        expect(function() {
          io = new IntersectionObserver(noop, {threshold: "foo"});
        }).to.throwException(/.*/i);
      });

    });


    describe('observe', function() {

      it('throws when target is not an Element', function() {
        expect(function() {
          io = new IntersectionObserver(noop);
          io.observe(null);
        }).to.throwException(/.*/i);
      });


      it('triggers if target intersects when observing begins', function(done) {
        io = new IntersectionObserver(function(records) {
          expect(records.length).to.be(1);
          expect(records[0].intersectionRatio).to.be(1);
          done();
        }, {root: rootEl});
        io.observe(targetEl1);
      });


      it('triggers with the correct arguments', function(done) {
        io = new IntersectionObserver(function(records, observer) {
          expect(records.length).to.be(1);
          expect(records[0] instanceof IntersectionObserverEntry).to.be.ok();
          expect(observer).to.be(io);
          expect(this).to.be(io);
          done();
        }, {root: rootEl});
        io.observe(targetEl1);
      });


      it('does trigger if target does not intersect when observing begins',
          function(done) {

        var spy = sinon.spy();
        io = new IntersectionObserver(spy, {root: rootEl});

        targetEl2.style.top = '-40px';
        io.observe(targetEl2);
        callDelayed(function() {
          expect(spy.callCount).to.be(1);
          done();
        }, ASYNC_TIMEOUT);
      });


      it('does not trigger if target is not a descendant of the intersection root in the containing block chain',
          function(done) {

        var spy = sinon.spy();
        io = new IntersectionObserver(spy, {root: parentEl});

        parentEl.style.position = 'static';
        io.observe(targetEl2);
        callDelayed(function() {
          expect(spy.callCount).to.be(0);
          done();
        }, ASYNC_TIMEOUT);
      });

      it('triggers if target or root becomes invisible',
          function(done) {

        var spy = sinon.spy();
        io = new IntersectionObserver(spy, {root: rootEl});

        runSequence([
          function(done) {
            io.observe(targetEl1);
            spy.waitForNotification(function() {
              expect(spy.callCount).to.be(1);
              var records = sortRecords(spy.lastCall.args[0]);
              expect(records.length).to.be(1);
              expect(records[0].intersectionRatio).to.be(1);
              done();
            }, ASYNC_TIMEOUT);
          },
          function(done) {
            targetEl1.style.display = 'none';
            spy.waitForNotification(function() {
              expect(spy.callCount).to.be(2);
              var records = sortRecords(spy.lastCall.args[0]);
              expect(records.length).to.be(1);
              expect(records[0].intersectionRatio).to.be(0);
              done();
            }, ASYNC_TIMEOUT);
          },
          function(done) {
            targetEl1.style.display = 'block';
            spy.waitForNotification(function() {
              expect(spy.callCount).to.be(3);
              var records = sortRecords(spy.lastCall.args[0]);
              expect(records.length).to.be(1);
              expect(records[0].intersectionRatio).to.be(1);
              done();
            }, ASYNC_TIMEOUT);
          },
          function(done) {
            rootEl.style.display = 'none';
            spy.waitForNotification(function() {
              expect(spy.callCount).to.be(4);
              var records = sortRecords(spy.lastCall.args[0]);
              expect(records.length).to.be(1);
              expect(records[0].intersectionRatio).to.be(0);
              done();
            }, ASYNC_TIMEOUT);
          },
          function(done) {
            rootEl.style.display = 'block';
            spy.waitForNotification(function() {
              expect(spy.callCount).to.be(5);
              var records = sortRecords(spy.lastCall.args[0]);
              expect(records.length).to.be(1);
              expect(records[0].intersectionRatio).to.be(1);
              done();
            }, ASYNC_TIMEOUT);
          },
        ], done);
      });


      it('handles container elements with non-visible overflow',
          function(done) {

        var spy = sinon.spy();
        io = new IntersectionObserver(spy, {root: rootEl});

        runSequence([
          function(done) {
            io.observe(targetEl1);
            spy.waitForNotification(function() {
              expect(spy.callCount).to.be(1);
              var records = sortRecords(spy.lastCall.args[0]);
              expect(records.length).to.be(1);
              expect(records[0].intersectionRatio).to.be(1);
              done();
            }, ASYNC_TIMEOUT);
          },
          function(done) {
            targetEl1.style.left = '-40px';
            spy.waitForNotification(function() {
              expect(spy.callCount).to.be(2);
              var records = sortRecords(spy.lastCall.args[0]);
              expect(records.length).to.be(1);
              expect(records[0].intersectionRatio).to.be(0);
              done();
            }, ASYNC_TIMEOUT);
          },
          function(done) {
            parentEl.style.overflow = 'visible';
            spy.waitForNotification(function() {
              expect(spy.callCount).to.be(3);
              var records = sortRecords(spy.lastCall.args[0]);
              expect(records.length).to.be(1);
              expect(records[0].intersectionRatio).to.be(1);
              done();
            }, ASYNC_TIMEOUT);
          }
        ], done);
      });


      it('observes one target at a single threshold correctly', function(done) {

        var spy = sinon.spy();
        io = new IntersectionObserver(spy, {root: rootEl, threshold: 0.5});

        runSequence([
          function(done) {
            targetEl1.style.left = '-5px';
            io.observe(targetEl1);
            spy.waitForNotification(function() {
              expect(spy.callCount).to.be(1);
              var records = sortRecords(spy.lastCall.args[0]);
              expect(records.length).to.be(1);
              expect(records[0].intersectionRatio).to.be.greaterThan(0.5);
              done();
            }, ASYNC_TIMEOUT);
          },
          function(done) {
            targetEl1.style.left = '-15px';
            spy.waitForNotification(function() {
              expect(spy.callCount).to.be(2);
              var records = sortRecords(spy.lastCall.args[0]);
              expect(records.length).to.be(1);
              expect(records[0].intersectionRatio).to.be.lessThan(0.5);
              done();
            }, ASYNC_TIMEOUT);
          },
          function(done) {
            targetEl1.style.left = '-25px';
            callDelayed(function() {
              expect(spy.callCount).to.be(2);
              done();
            }, ASYNC_TIMEOUT);
          },
          function(done) {
            targetEl1.style.left = '-10px';
            spy.waitForNotification(function() {
              expect(spy.callCount).to.be(3);
              var records = sortRecords(spy.lastCall.args[0]);
              expect(records.length).to.be(1);
              expect(records[0].intersectionRatio).to.be(0.5);
              done();
            }, ASYNC_TIMEOUT);
          }
        ], done);

      });


      it('observes multiple targets at multiple thresholds correctly',
          function(done) {

        var spy = sinon.spy();
        io = new IntersectionObserver(spy, {
          root: rootEl,
          threshold: [1, 0.5, 0]
        });

        runSequence([
          function(done) {
            targetEl1.style.top = '0px';
            targetEl1.style.left = '-15px';
            targetEl2.style.top = '-5px';
            targetEl2.style.left = '0px';
            targetEl3.style.top = '0px';
            targetEl3.style.left = '205px';
            io.observe(targetEl1);
            io.observe(targetEl2);
            io.observe(targetEl3);
            spy.waitForNotification(function() {
              expect(spy.callCount).to.be(1);
              var records = sortRecords(spy.lastCall.args[0]);
              expect(records.length).to.be(3);
              expect(records[0].target).to.be(targetEl1);
              expect(records[0].intersectionRatio).to.be(0.25);
              expect(records[1].target).to.be(targetEl2);
              expect(records[1].intersectionRatio).to.be(0.75);
              done();
            }, ASYNC_TIMEOUT);
          },
          function(done) {
            targetEl1.style.top = '0px';
            targetEl1.style.left = '-5px';
            targetEl2.style.top = '-15px';
            targetEl2.style.left = '0px';
            targetEl3.style.top = '0px';
            targetEl3.style.left = '195px';
            spy.waitForNotification(function() {
              expect(spy.callCount).to.be(2);
              var records = sortRecords(spy.lastCall.args[0]);
              expect(records.length).to.be(3);
              expect(records[0].target).to.be(targetEl1);
              expect(records[0].intersectionRatio).to.be(0.75);
              expect(records[1].target).to.be(targetEl2);
              expect(records[1].intersectionRatio).to.be(0.25);
              expect(records[2].target).to.be(targetEl3);
              expect(records[2].intersectionRatio).to.be(0.25);
              done();
            }, ASYNC_TIMEOUT);
          },
          function(done) {
            targetEl1.style.top = '0px';
            targetEl1.style.left = '5px';
            targetEl2.style.top = '-25px';
            targetEl2.style.left = '0px';
            targetEl3.style.top = '0px';
            targetEl3.style.left = '185px';
            spy.waitForNotification(function() {
              expect(spy.callCount).to.be(3);
              var records = sortRecords(spy.lastCall.args[0]);
              expect(records.length).to.be(3);
              expect(records[0].target).to.be(targetEl1);
              expect(records[0].intersectionRatio).to.be(1);
              expect(records[1].target).to.be(targetEl2);
              expect(records[1].intersectionRatio).to.be(0);
              expect(records[2].target).to.be(targetEl3);
              expect(records[2].intersectionRatio).to.be(0.75);
              done();
            }, ASYNC_TIMEOUT);
          },
          function(done) {
            targetEl1.style.top = '0px';
            targetEl1.style.left = '15px';
            targetEl2.style.top = '-35px';
            targetEl2.style.left = '0px';
            targetEl3.style.top = '0px';
            targetEl3.style.left = '175px';
            spy.waitForNotification(function() {
              expect(spy.callCount).to.be(4);
              var records = sortRecords(spy.lastCall.args[0]);
              expect(records.length).to.be(1);
              expect(records[0].target).to.be(targetEl3);
              expect(records[0].intersectionRatio).to.be(1);
              done();
            }, ASYNC_TIMEOUT);
          }
        ], done);
      });


      it('handles rootMargin properly', function(done) {

        parentEl.style.overflow = 'visible';
        targetEl1.style.top = '0px';
        targetEl1.style.left = '-20px';
        targetEl2.style.top = '-20px';
        targetEl2.style.left = '0px';
        targetEl3.style.top = '0px';
        targetEl3.style.left = '200px';
        targetEl4.style.top = '180px';
        targetEl4.style.left = '180px';

        runSequence([
          function(done) {
            io = new IntersectionObserver(function(records) {
              records = sortRecords(records);
              expect(records.length).to.be(4);
              expect(records[0].target).to.be(targetEl1);
              expect(records[0].intersectionRatio).to.be(1);
              expect(records[1].target).to.be(targetEl2);
              expect(records[1].intersectionRatio).to.be(.5);
              expect(records[2].target).to.be(targetEl3);
              expect(records[2].intersectionRatio).to.be(.5);
              expect(records[3].target).to.be(targetEl4);
              expect(records[3].intersectionRatio).to.be(1);
              io.disconnect();
              done();
            }, {root: rootEl, rootMargin: '10px'});

            io.observe(targetEl1);
            io.observe(targetEl2);
            io.observe(targetEl3);
            io.observe(targetEl4);
          },
          function(done) {
            io = new IntersectionObserver(function(records) {
              records = sortRecords(records);
              expect(records.length).to.be(3);
              expect(records[0].target).to.be(targetEl1);
              expect(records[0].intersectionRatio).to.be(0.5);
              expect(records[2].target).to.be(targetEl3);
              expect(records[2].intersectionRatio).to.be(0.5);
              expect(records[3].target).to.be(targetEl4);
              expect(records[3].intersectionRatio).to.be(0.5);
              io.disconnect();
              done();
            }, {root: rootEl, rootMargin: '-10px 10%'});

            io.observe(targetEl1);
            io.observe(targetEl2);
            io.observe(targetEl3);
            io.observe(targetEl4);
          },
          function(done) {
            io = new IntersectionObserver(function(records) {
              records = sortRecords(records);
              expect(records.length).to.be(4);
              expect(records[0].target).to.be(targetEl1);
              expect(records[0].intersectionRatio).to.be(0.5);
              expect(records[3].target).to.be(targetEl4);
              expect(records[3].intersectionRatio).to.be(0.5);
              io.disconnect();
              done();
            }, {root: rootEl, rootMargin: '-5% -2.5% 0px'});

            io.observe(targetEl1);
            io.observe(targetEl2);
            io.observe(targetEl3);
            io.observe(targetEl4);
          },
          function(done) {
            io = new IntersectionObserver(function(records) {
              records = sortRecords(records);
              expect(records.length).to.be(4);
              expect(records[0].target).to.be(targetEl1);
              expect(records[0].intersectionRatio).to.be(0.5);
              expect(records[1].target).to.be(targetEl2);
              expect(records[1].intersectionRatio).to.be(0.5);
              expect(records[3].target).to.be(targetEl4);
              expect(records[3].intersectionRatio).to.be(0.25);
              io.disconnect();
              done();
            }, {root: rootEl, rootMargin: '5% -2.5% -10px -190px'});

            io.observe(targetEl1);
            io.observe(targetEl2);
            io.observe(targetEl3);
            io.observe(targetEl4);
          }
        ], done);
      });


      it('handles targets on the boundary of root', function(done) {

        var spy = sinon.spy();
        io = new IntersectionObserver(spy, {root: rootEl});

        runSequence([
          function(done) {
            targetEl1.style.top = '0px';
            targetEl1.style.left = '-21px';
            targetEl2.style.top = '-20px';
            targetEl2.style.left = '0px';
            io.observe(targetEl1);
            io.observe(targetEl2);
            spy.waitForNotification(function() {
              expect(spy.callCount).to.be(1);
              var records = sortRecords(spy.lastCall.args[0]);
              expect(records.length).to.be(2);
              expect(records[1].intersectionRatio).to.be(0);
              expect(records[1].target).to.be(targetEl2);
              done();
            }, ASYNC_TIMEOUT);
          },
          function(done) {
            targetEl1.style.top = '0px';
            targetEl1.style.left = '-20px';
            targetEl2.style.top = '-21px';
            targetEl2.style.left = '0px';
            spy.waitForNotification(function() {
              expect(spy.callCount).to.be(2);
              var records = sortRecords(spy.lastCall.args[0]);
              expect(records.length).to.be(2);
              expect(records[0].intersectionRatio).to.be(0);
              expect(records[0].target).to.be(targetEl1);
              expect(records[1].intersectionRatio).to.be(0);
              expect(records[1].target).to.be(targetEl2);
              done();
            }, ASYNC_TIMEOUT);
          },
          function(done) {
            targetEl1.style.top = '-20px';
            targetEl1.style.left = '200px';
            targetEl2.style.top = '200px';
            targetEl2.style.left = '200px';
            spy.waitForNotification(function() {
              expect(spy.callCount).to.be(3);
              var records = sortRecords(spy.lastCall.args[0]);
              expect(records.length).to.be(1);
              expect(records[0].intersectionRatio).to.be(0);
              expect(records[0].target).to.be(targetEl2);
              done();
            }, ASYNC_TIMEOUT);
          },
          function(done) {
            targetEl3.style.top = '20px';
            targetEl3.style.left = '-20px';
            targetEl4.style.top = '-20px';
            targetEl4.style.left = '20px';
            io.observe(targetEl3);
            io.observe(targetEl4);
            spy.waitForNotification(function() {
              expect(spy.callCount).to.be(4);
              var records = sortRecords(spy.lastCall.args[0]);
              expect(records.length).to.be(2);
              expect(records[0].intersectionRatio).to.be(0);
              expect(records[0].target).to.be(targetEl3);
              expect(records[1].intersectionRatio).to.be(0);
              expect(records[1].target).to.be(targetEl4);
              done();
            }, ASYNC_TIMEOUT);
          }
        ], done);

      });


      it('handles zero-size targets within the root coordinate space',
          function(done) {

        io = new IntersectionObserver(function(records) {
          expect(records.length).to.be(1);
          expect(records[0].intersectionRatio).to.be(0);
          done();
        }, {root: rootEl});

        targetEl1.style.top = '0px';
        targetEl1.style.left = '0px';
        targetEl1.style.width = '0px';
        targetEl1.style.height = '0px';
        io.observe(targetEl1);
      });


      it('handles root/target elements not yet in the DOM', function(done) {

        rootEl.parentNode.removeChild(rootEl);
        targetEl1.parentNode.removeChild(targetEl1);

        var spy = sinon.spy();
        io = new IntersectionObserver(spy, {root: rootEl});

        runSequence([
          function(done) {
            io.observe(targetEl1);
            callDelayed(done, 0);
          },
          function(done) {
            document.getElementById('fixtures').appendChild(rootEl);
            callDelayed(function() {
              expect(spy.callCount).to.be(1);
              done();
            }, ASYNC_TIMEOUT);
          },
          function(done) {
            parentEl.insertBefore(targetEl1, targetEl2);
            spy.waitForNotification(function() {
              expect(spy.callCount).to.be(2);
              var records = sortRecords(spy.lastCall.args[0]);
              expect(records.length).to.be(1);
              expect(records[0].intersectionRatio).to.be(1);
              expect(records[0].target).to.be(targetEl1);
              done();
            }, ASYNC_TIMEOUT);
          },
          function(done) {
            grandParentEl.parentNode.removeChild(grandParentEl);
            spy.waitForNotification(function() {
              expect(spy.callCount).to.be(3);
              var records = sortRecords(spy.lastCall.args[0]);
              expect(records.length).to.be(1);
              expect(records[0].intersectionRatio).to.be(0);
              expect(records[0].target).to.be(targetEl1);
              done();
            }, ASYNC_TIMEOUT);
          },
          function(done) {
            rootEl.appendChild(targetEl1);
            spy.waitForNotification(function() {
              expect(spy.callCount).to.be(4);
              var records = sortRecords(spy.lastCall.args[0]);
              expect(records.length).to.be(1);
              expect(records[0].intersectionRatio).to.be(1);
              expect(records[0].target).to.be(targetEl1);
              done();
            }, ASYNC_TIMEOUT);
          },
          function(done) {
            rootEl.parentNode.removeChild(rootEl);
            spy.waitForNotification(function() {
              expect(spy.callCount).to.be(5);
              var records = sortRecords(spy.lastCall.args[0]);
              expect(records.length).to.be(1);
              expect(records[0].intersectionRatio).to.be(0);
              expect(records[0].target).to.be(targetEl1);
              done();
            }, ASYNC_TIMEOUT);
          }
        ], done);
      });


      it('handles sub-root element scrolling', function(done) {
        io = new IntersectionObserver(function(records) {
          expect(records.length).to.be(1);
          expect(records[0].intersectionRatio).to.be(1);
          done();
        }, {root: rootEl});

        io.observe(targetEl3);
        callDelayed(function() {
          parentEl.scrollLeft = 40;
        }, 0);
      });


      it('supports CSS transitions and transforms', function(done) {

        targetEl1.style.top = '220px';
        targetEl1.style.left = '220px';
        
        var callCount = 0;

        io = new IntersectionObserver(function(records) {
          callCount++;
          if (callCount <= 1) {
            return;
          }
          expect(records.length).to.be(1);
          expect(records[0].intersectionRatio).to.be(1);
          done();
        }, {root: rootEl, threshold: [1]});

        io.observe(targetEl1);
        callDelayed(function() {
          targetEl1.style.transform = 'translateX(-40px) translateY(-40px)';
        }, 0);
      });


      it('uses the viewport when no root is specified', function(done) {
        window.onmessage = function (e) {
          expect(e.data).to.be.ok();
          win.close();
          done();
        };

        var win = window.open("intersectionobserver_window.html");
      });

      it('triggers only once if observed multiple times (and does not crash when collected)', function(done) {
        var spy = sinon.spy();
        io = new IntersectionObserver(spy, {root: rootEl});
        io.observe(targetEl1);
        io.observe(targetEl1);
        io.observe(targetEl1);

        callDelayed(function () {
          expect(spy.callCount).to.be(1);
          done();
        }, ASYNC_TIMEOUT);
      });

    });

    describe('observe subframe', function () {
      
      it('should not trigger if target and root are not in the same document',
          function(done) {

        var spy = sinon.spy();
        io = new IntersectionObserver(spy, {root: rootEl});

        targetEl4.onload = function () {
          targetEl5 = targetEl4.contentDocument.getElementById('target5');
          io.observe(targetEl5);
          callDelayed(function() {
            expect(spy.callCount).to.be(0);
            done();
          }, ASYNC_TIMEOUT);
        }

        targetEl4.src = "intersectionobserver_iframe.html";
      
      });

      it('boundingClientRect matches target.getBoundingClientRect() for an element inside an iframe',
          function(done) {

        io = new IntersectionObserver(function(records) {
          expect(records.length).to.be(1);
          expect(records[0].boundingClientRect.top, targetEl5.getBoundingClientRect().top);
          expect(records[0].boundingClientRect.left, targetEl5.getBoundingClientRect().left);
          expect(records[0].boundingClientRect.width, targetEl5.getBoundingClientRect().width);
          expect(records[0].boundingClientRect.height, targetEl5.getBoundingClientRect().height);
          done();
        }, {threshold: [1]});

        targetEl4.onload = function () {
          targetEl5 = targetEl4.contentDocument.getElementById('target5');
          io.observe(targetEl5);
        }

        targetEl4.src = "intersectionobserver_iframe.html";
      });

      it('rootBounds should is set to null for cross-origin observations', function(done) {

        window.onmessage = function (e) {
          expect(e.data).to.be.ok();
          done();
        };

        targetEl4.src = "http://example.org/tests/dom/base/test/intersectionobserver_iframe.html";

      });
    
    });

    describe('takeRecords', function() {

      it('supports getting records before the callback is invoked',
          function(done) {

        var lastestRecords = [];
        io = new IntersectionObserver(function(records) {
          lastestRecords = lastestRecords.concat(records);
        }, {root: rootEl});
        io.observe(targetEl1);

        window.requestAnimationFrame && requestAnimationFrame(function() {
          lastestRecords = lastestRecords.concat(io.takeRecords());
        });

        callDelayed(function() {
          expect(lastestRecords.length).to.be(1);
          expect(lastestRecords[0].intersectionRatio).to.be(1);
          done();
        }, ASYNC_TIMEOUT);
      });

    });


    describe('unobserve', function() {

      it('removes targets from the internal store', function(done) {

        var spy = sinon.spy();
        io = new IntersectionObserver(spy, {root: rootEl});

        runSequence([
          function(done) {
            targetEl1.style.top = targetEl2.style.top = '0px';
            targetEl1.style.left = targetEl2.style.left = '0px';
            io.observe(targetEl1);
            io.observe(targetEl2);
            spy.waitForNotification(function() {
              expect(spy.callCount).to.be(1);
              var records = sortRecords(spy.lastCall.args[0]);
              expect(records.length).to.be(2);
              expect(records[0].target).to.be(targetEl1);
              expect(records[0].intersectionRatio).to.be(1);
              expect(records[1].target).to.be(targetEl2);
              expect(records[1].intersectionRatio).to.be(1);
              done();
            }, ASYNC_TIMEOUT);
          },
          function(done) {
            io.unobserve(targetEl1);
            targetEl1.style.top = targetEl2.style.top = '0px';
            targetEl1.style.left = targetEl2.style.left = '-40px';
            spy.waitForNotification(function() {
              expect(spy.callCount).to.be(2);
              var records = sortRecords(spy.lastCall.args[0]);
              expect(records.length).to.be(1);
              expect(records[0].target).to.be(targetEl2);
              expect(records[0].intersectionRatio).to.be(0);
              done();
            }, ASYNC_TIMEOUT);
          },
          function(done) {
            io.unobserve(targetEl2);
            targetEl1.style.top = targetEl2.style.top = '0px';
            targetEl1.style.left = targetEl2.style.left = '0px';
            callDelayed(function() {
              expect(spy.callCount).to.be(2);
              done();
            }, ASYNC_TIMEOUT);
          }
        ], done);

      });

    });

    describe('disconnect', function() {

      it('removes all targets and stops listening for changes', function(done) {

        var spy = sinon.spy();
        io = new IntersectionObserver(spy, {root: rootEl});

        runSequence([
          function(done) {
            targetEl1.style.top = targetEl2.style.top = '0px';
            targetEl1.style.left = targetEl2.style.left = '0px';
            io.observe(targetEl1);
            io.observe(targetEl2);
            spy.waitForNotification(function() {
              expect(spy.callCount).to.be(1);
              var records = sortRecords(spy.lastCall.args[0]);
              expect(records.length).to.be(2);
              expect(records[0].target).to.be(targetEl1);
              expect(records[0].intersectionRatio).to.be(1);
              expect(records[1].target).to.be(targetEl2);
              expect(records[1].intersectionRatio).to.be(1);
              done();
            }, ASYNC_TIMEOUT);
          },
          function(done) {
            io.disconnect();
            targetEl1.style.top = targetEl2.style.top = '0px';
            targetEl1.style.left = targetEl2.style.left = '-40px';
            callDelayed(function() {
              expect(spy.callCount).to.be(1);
              done();
            }, ASYNC_TIMEOUT);
          }
        ], done);

      });

    });

  });


  /**
   * Runs a sequence of function and when finished invokes the done callback.
   * Each function in the sequence is invoked with its own done function and
   * it should call that function once it's complete.
   * @param {Array<Function>} functions An array of async functions.
   * @param {Function} done A final callback to be invoked once all function
   *     have run.
   */
  function runSequence(functions, done) {
    var next = functions.shift();
    if (next) {
      next(function() {
        runSequence(functions, done);
      });
    } else {
      done && done();
    }
  }


  /**
   * Sorts an array of records alphebetically by ascending ID. Since the current
   * native implementation doesn't sort change entries by `observe` order, we do
   * that ourselves for the non-polyfill case. Since all tests call observe
   * on targets in sequential order, this should always match.
   * https://crbug.com/613679
   * @param {Array<IntersectionObserverEntry>} entries The entries to sort.
   * @return {Array<IntersectionObserverEntry>} The sorted array.
   */
  function sortRecords(entries) {
    entries = entries.sort(function(a, b) {
      return a.target.id < b.target.id ? -1 : 1;
    });
    return entries;
  }


  /**
   * Adds the common styles used by all tests to the page.
   */
  function addStyles() {
    var styles = document.createElement('style');
    styles.id = 'styles';
    document.documentElement.appendChild(styles);

    var cssText =
        '#root {' +
        '  position: relative;' +
        '  width: 400px;' +
        '  height: 200px;' +
        '  background: #eee' +
        '}' +
        '#grand-parent {' +
        '  position: relative;' +
        '  width: 200px;' +
        '  height: 200px;' +
        '}' +
        '#parent {' +
        '  position: absolute;' +
        '  top: 0px;' +
        '  left: 200px;' +
        '  overflow: hidden;' +
        '  width: 200px;' +
        '  height: 200px;' +
        '  background: #ddd;' +
        '}' +
        '#target1, #target2, #target3, #target4 {' +
        '  position: absolute;' +
        '  top: 0px;' +
        '  left: 0px;' +
        '  width: 20px;' +
        '  height: 20px;' +
        '  transform: translateX(0px) translateY(0px);' +
        '  transition: transform .5s;' +
        '  background: #f00;' +
        '  border: none;' +
        '}';

    styles.innerHTML = cssText;
  }


  /**
   * Adds the DOM fixtures used by all tests to the page and assigns them to
   * global variables so they can be referenced within the tests.
   */
  function addFixtures() {
    var fixtures = document.createElement('div');
    fixtures.id = 'fixtures';

    fixtures.innerHTML =
        '<div id="root">' +
        '  <div id="grand-parent">' +
        '    <div id="parent">' +
        '      <div id="target1"></div>' +
        '      <div id="target2"></div>' +
        '      <div id="target3"></div>' +
        '      <iframe id="target4"></iframe>' +
        '    </div>' +
        '  </div>' +
        '</div>';

    document.body.appendChild(fixtures);

    rootEl = document.getElementById('root');
    grandParentEl = document.getElementById('grand-parent');
    parentEl = document.getElementById('parent');
    targetEl1 = document.getElementById('target1');
    targetEl2 = document.getElementById('target2');
    targetEl3 = document.getElementById('target3');
    targetEl4 = document.getElementById('target4');
  }


  /**
   * Removes the common styles from the page.
   */
  function removeStyles() {
    var styles = document.getElementById('styles');
    styles.parentNode.removeChild(styles);
  }


  /**
   * Removes the DOM fixtures from the page and resets the global references.
   */
  function removeFixtures() {
    var fixtures = document.getElementById('fixtures');
    fixtures.parentNode.removeChild(fixtures);

    rootEl = null;
    grandParentEl = null;
    parentEl = null;
    targetEl1 = null;
    targetEl2 = null;
    targetEl3 = null;
    targetEl4 = null;
  }

  SimpleTest.waitForExplicitFinish();
</script>
</pre>
<div id="log">
</div>
</body>
</html>
